{"$schema":"https://ui.shadcn.com/schema/registry-item.json","dependencies":["@audio-ui/react"],"description":"An editor for a breakpoint ramp, such as an ADSR shape or an automation curve.","files":[{"content":"\"use client\";\n\nimport { Envelope as EnvelopePrimitive } from \"@audio-ui/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { type CSSProperties, useCallback } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst envelopeVariants = cva(\"\", {\n  defaultVariants: {\n    size: \"default\",\n  },\n  variants: {\n    size: {\n      default: \"h-48\",\n      lg: \"h-64\",\n      sm: \"h-32\",\n    },\n  },\n});\n\n/** The frame's room for the end points, which the grid's lines are measured from. */\nconst INSET = \"0.875rem\";\nconst DEFAULT_GRID_STEP = 0.25;\n/** A segment's bend as a signed percentage: positive holds the start of the segment, negative the end. */\nconst describeCurve = (curve = 0) =>\n  `Curve ${curve > 0 ? \"+\" : \"\"}${Math.round(curve * 100)}%`;\n/** Above this level a point's label sits under it, not over it. */\nconst LABEL_FLIP_ABOVE = 0.8;\n\n/** A line every `step` across and down, spread over the whole frame like a plot's: they sit on the data's own coordinates, so a point is on a line at 0, ¼, ½, ¾ and 1. */\nfunction gridLines(step: number): CSSProperties {\n  const share = `calc((100% - 2 * ${INSET}) * ${step})`;\n  return {\n    backgroundImage:\n      \"linear-gradient(to right, var(--border) 1px, transparent 1px), linear-gradient(to bottom, var(--border) 1px, transparent 1px)\",\n    backgroundPosition: `${INSET} 0, 0 ${INSET}`,\n    backgroundSize: `${share} 100%, 100% ${share}`,\n  };\n}\n\ntype Domain = readonly [number, number];\n\n/** A share of the axis as a value in the axis's own units, to three significant digits. */\nfunction describeAxis(share: number, domain?: Domain, unit = \"\") {\n  const [min, max] = domain ?? [0, 100];\n  const value = Number((min + share * (max - min)).toPrecision(3));\n  const text = domain ? String(value) : `${Math.round(share * 100)}`;\n  const suffix = domain ? unit : \"%\";\n  return suffix === \"%\" ? `${text}%` : `${text} ${suffix}`.trim();\n}\n\ninterface EnvelopeProps\n  extends EnvelopePrimitive.Root.Props,\n    VariantProps<typeof envelopeVariants> {\n  /** Whether a handle on each segment bends it. @default true */\n  curves?: boolean;\n  /** Prints a point's value in the label that shows while it is dragged or focused, and in its accessible value text. Defaults to the values in `xDomain` and `yDomain`, or percentages. */\n  formatValue?: (point: { x: number; y: number }) => string;\n  /** The values the horizontal axis runs from and to, for the labels: `[0, 2000]` with `xUnit=\"ms\"`. Unset, it reads in percent. */\n  xDomain?: Domain;\n  xUnit?: string;\n  /** The values the vertical axis runs from and to, for the labels. */\n  yDomain?: Domain;\n  yUnit?: string;\n}\n\n/**\n * The frame has room inside it, so the first and last points sit within it\n * instead of on its edge. Drag a point to move it, the small handle on a\n * segment to bend it, and hold Shift for a fine drag. With `snap`, the grid's\n * lines are the snapping ones.\n */\nexport function Envelope({\n  className,\n  curves = true,\n  formatValue,\n  size,\n  snap,\n  xDomain,\n  xUnit,\n  yDomain,\n  yUnit,\n  ...props\n}: EnvelopeProps) {\n  const describe = useCallback(\n    (point: { x: number; y: number }) =>\n      formatValue\n        ? formatValue(point)\n        : `${describeAxis(point.x, xDomain, xUnit)} · ${describeAxis(point.y, yDomain, yUnit)}`,\n    [formatValue, xDomain, xUnit, yDomain, yUnit]\n  );\n\n  return (\n    <div\n      className={cn(\n        \"rounded-xl border border-transparent bg-card ring-1 ring-foreground/10 transition-[color,border-color,box-shadow,opacity] duration-150 ease-out hover:ring-3 hover:ring-ring/50 has-focus-visible:ring-3 has-focus-visible:ring-ring/50 active:ring-3 active:ring-ring/50 has-focus-visible:border-ring active:border-ring motion-reduce:transition-none group/envelope box-border w-full cursor-crosshair touch-none select-none p-3.5\",\n        \"has-data-disabled:pointer-events-none has-data-disabled:opacity-50\",\n        envelopeVariants({ size }),\n        className\n      )}\n      data-slot=\"envelope\"\n      style={gridLines(snap && snap > 0 ? snap : DEFAULT_GRID_STEP)}\n    >\n      <EnvelopePrimitive.Root\n        className=\"size-full data-disabled:cursor-not-allowed\"\n        role=\"group\"\n        snap={snap}\n        {...props}\n      >\n        <EnvelopePrimitive.Graph className=\"size-full overflow-visible\">\n          <EnvelopePrimitive.Fill className=\"fill-primary/15 stroke-none\" />\n          <EnvelopePrimitive.Line className=\"stroke-primary stroke-2 [stroke-linejoin:round]\" />\n        </EnvelopePrimitive.Graph>\n        {curves ? (\n          <EnvelopePrimitive.Segments>\n            {(point, index) => (\n              <EnvelopePrimitive.Segment\n                aria-label={`Curve ${index + 1}`}\n                className=\"size-3 rounded-full border border-primary bg-background shadow-xs transition-[box-shadow,opacity] hover:ring-4 hover:ring-ring/30 focus-visible:ring-4 focus-visible:ring-ring/30 data-dragging:ring-4 data-dragging:ring-ring/30 data-flat:hidden motion-reduce:transition-none group/segment cursor-ns-resize opacity-0 outline-none after:absolute after:-inset-2 pointer-coarse:opacity-100 pointer-coarse:after:-inset-4 group-focus-within/envelope:opacity-100 group-hover/envelope:opacity-100 data-dragging:opacity-100\"\n                index={index}\n                key={index}\n              >\n                <span\n                  aria-hidden=\"true\"\n                  className=\"rounded-full bg-popover px-1.5 py-0.5 font-mono text-[10px] text-popover-foreground shadow-md ring-1 ring-foreground/10 -translate-x-1/2 pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 hidden whitespace-nowrap group-data-dragging/segment:block\"\n                >\n                  {describeCurve(point.curve)}\n                </span>\n              </EnvelopePrimitive.Segment>\n            )}\n          </EnvelopePrimitive.Segments>\n        ) : null}\n        <EnvelopePrimitive.Points>\n          {(point, index, points) => (\n            <EnvelopePrimitive.Point\n              aria-label={`Point ${index + 1} of ${points.length}`}\n              className=\"rounded-full border-2 border-primary bg-background shadow-xs data-active:bg-primary transition-[box-shadow] duration-150 ease-out hover:ring-4 hover:ring-ring/30 focus-within:ring-4 focus-within:ring-ring/30 data-dragging:ring-4 data-dragging:ring-ring/30 motion-reduce:transition-none group/point size-4 cursor-grab data-dragging:bg-primary focus-visible:bg-primary touch-none select-none outline-none after:absolute after:-inset-2 pointer-coarse:after:-inset-3.5 active:cursor-grabbing\"\n              getAriaValueText={describe}\n              index={index}\n              key={index}\n            >\n              <span\n                aria-hidden=\"true\"\n                className={cn(\n                  \"rounded-full bg-popover px-1.5 py-0.5 font-mono text-[10px] text-popover-foreground shadow-md ring-1 ring-foreground/10 -translate-x-1/2 pointer-events-none absolute left-1/2 z-10 hidden whitespace-nowrap group-hover/point:block group-data-dragging/point:block group-focus-visible/point:block\",\n                  // Near the top the label goes under the point, so it stays inside the page.\n                  point.y > LABEL_FLIP_ABOVE\n                    ? \"top-full mt-2\"\n                    : \"bottom-full mb-2\"\n                )}\n              >\n                {describe(point)}\n              </span>\n            </EnvelopePrimitive.Point>\n          )}\n        </EnvelopePrimitive.Points>\n      </EnvelopePrimitive.Root>\n    </div>\n  );\n}","path":"envelope.tsx","target":"components/audio-ui/envelope.tsx","type":"registry:ui"}],"name":"envelope","type":"registry:ui"}